﻿using Sun.Wadl;
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Xml.Serialization;
using System.Net;
using System.Xml;

namespace Hbrs.Soa.Restclient.WadlToCode
{
    public enum GeneratedFileType
    {
        ResourceClass,
        ServiceContract,
        ServiceClient
    }
    
    public class WadlToCodeGenerator
    {
        #region member variables

        private String m_CodeNamespaceName;

        private String m_WadlFileName;

        private String m_RescourceClassName;
        
        private CodeNamespace m_RescourceClass;

        private CodeNamespace m_ServiceClient;

        private CodeNamespace m_ServiceContract;
       
        #endregion

        #region private methods

        private CodeNamespace GetNamespaceFor(GeneratedFileType fileType)
        {
            CodeNamespace GeneratedCodeNamespace = null;
            switch (fileType)
            {
                case GeneratedFileType.ResourceClass:
                    GeneratedCodeNamespace = m_RescourceClass;
                    break;
                case GeneratedFileType.ServiceContract:
                    GeneratedCodeNamespace = m_ServiceContract;
                    break;
                case GeneratedFileType.ServiceClient:
                    GeneratedCodeNamespace = m_ServiceClient;
                    break;
            }
            return GeneratedCodeNamespace;
        }

        private String GetClassNameFor(GeneratedFileType fileType)
        {
            String outputFileName = GetInputFileName();
            switch (fileType)
            {
                case GeneratedFileType.ResourceClass:
                    outputFileName = m_RescourceClassName;
                    break;
                case GeneratedFileType.ServiceContract:
                    outputFileName = (outputFileName.ToUpperInvariant().StartsWith("I")) ? outputFileName : "I" + outputFileName;
                    break;
                case GeneratedFileType.ServiceClient:
                    outputFileName = (outputFileName.ToUpperInvariant().StartsWith("I")) ? outputFileName.Remove(0, 1) : outputFileName;
                    outputFileName = outputFileName + "Client";
                    break;
            }
            return outputFileName;
        }

        private static CodeNamespace CreateResourceClassFromWadl(application app, String Namespace)
            {
            CodeNamespace ns = new CodeNamespace(Namespace);
            ns.Imports.Add(new CodeNamespaceImport("System"));
            if (app.grammars != null)
            {
                if (app.grammars.include != null)
                {
                    foreach (include inc in app.grammars.include)
                    {
                        ns = Hbrs.Soa.Restclient.Serializing.SchemaSerializer.ImportSchemasAsClasses(inc.href,Namespace);
                    }
                }
            }
            return ns;            
        }

        private CodeNamespace CreateServiceContractInterfaceFromWadl(application WadlRoot, String Namespace)
        {
            CodeNamespace ns = new CodeNamespace(Namespace);
            // Using Statements
            ns.Imports.Add(new CodeNamespaceImport("System"));
            ns.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
            ns.Imports.Add(new CodeNamespaceImport("System.ServiceModel"));
            ns.Imports.Add(new CodeNamespaceImport("System.ServiceModel.Web"));

            CodeTypeDeclaration NewInterface = new CodeTypeDeclaration();
            NewInterface.IsInterface = true;
            NewInterface.Name = GetClassNameFor(GeneratedFileType.ServiceContract);
            NewInterface.Attributes = MemberAttributes.Public;
            NewInterface.CustomAttributes.Add(new CodeAttributeDeclaration("ServiceContract"));

            if (WadlRoot.resources != null)
            {   // application contains zero or...
                foreach (resources WadlResources in WadlRoot.resources)
                {   // ... more resources elements   
                    for (int i = 0; i < WadlResources.resource.Length; i++)
                    {
                        resource WadlResource = WadlResources.resource[i];
                        if (WadlResource.id != null)
                        {   //  Interface TItem Type...
                            m_RescourceClassName = WadlResource.id;
                            CodeTypeParameter RescourceItem = new CodeTypeParameter("TItem");
                            RescourceItem.Constraints.Add(new CodeTypeReference(WadlResource.id, null));
                            NewInterface.TypeParameters.Add(RescourceItem);
                        }
                        else
                        {
                            throw new ArgumentException(
                                String.Format(
                                    System.Globalization.CultureInfo.InvariantCulture,
                                    "Wadl File contains a rescource element with no parameter ID."
                                ));
                        }

                        if (WadlResource.Items != null)
                        {
                            foreach (var unknownElement in WadlResource.Items)
                            {
                                Sun.Wadl.method WadlMethod = unknownElement as method;
                                if (WadlMethod != null)
                                {
                                    CodeMemberMethod NewInterfaceMethod = GetMethodDefinitionFromWadl(WadlMethod,WadlRoot);
                                    if (NewInterfaceMethod != null)
                                    {
                                        NewInterface.Members.Add(NewInterfaceMethod);    
                                    }
                                }
                            }
                        }
                    }
                }
            }   

            ns.Types.Add(NewInterface);
            return ns;
        }
       
        private CodeNamespace CreateServiceClientImplementation(String Namespace)
        {
            CodeNamespace ns = new CodeNamespace(Namespace);
            // Using Statements
            ns.Imports.Add(new CodeNamespaceImport("System"));
            ns.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
            ns.Imports.Add(new CodeNamespaceImport("System.ServiceModel"));
            ns.Imports.Add(new CodeNamespaceImport("System.ServiceModel.Web"));
            ns.Imports.Add(new CodeNamespaceImport("Hbrs.Soa.Restclient.Client"));

            CodeTypeDeclaration NewClass = new CodeTypeDeclaration();
            NewClass.IsClass = true;
            NewClass.Name = GetClassNameFor(GeneratedFileType.ServiceClient);
            NewClass.Attributes = MemberAttributes.Public;
           
            foreach (CodeTypeDeclaration IDeclaration in m_ServiceContract.Types)
            {   // Interface<Type> Declaration
                CodeTypeReference ParentClass = new CodeTypeReference("RestStarterClientBase");
                CodeTypeReference InterfaceType = new CodeTypeReference(IDeclaration.Name);
                for (int i = 0; i < IDeclaration.TypeParameters.Count; i++)
                    for (int j = 0; j < IDeclaration.TypeParameters[i].Constraints.Count; j++) {
                        ParentClass.TypeArguments.Add(IDeclaration.TypeParameters[i].Constraints[j].BaseType);
                        InterfaceType.TypeArguments.Add(IDeclaration.TypeParameters[i].Constraints[j].BaseType);
                    }
                NewClass.BaseTypes.Add(ParentClass);
                NewClass.BaseTypes.Add(InterfaceType);
                
                // Member Methods
                foreach (CodeMemberMethod IMethod in m_ServiceContract.Types[0].Members)
                {
                    NewClass.Members.Add(CreateServiceClientMethods(IMethod, InterfaceType));
                }

                // Declares a default constructor that takes no arguments.
                CodeConstructor defaultConstructor = new CodeConstructor();
                defaultConstructor.Attributes = MemberAttributes.Public;
                NewClass.Members.Add(defaultConstructor);

                // Declare a constructor that takes a string argument and calls the base class constructor with it.
                CodeConstructor baseUriConstructor = new CodeConstructor();
                baseUriConstructor.Attributes = MemberAttributes.Public;
                baseUriConstructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(Uri), "baseUri"));
                baseUriConstructor.BaseConstructorArgs.Add(new CodeVariableReferenceExpression("baseUri"));
                NewClass.Members.Add(baseUriConstructor);

                // Declare a constructor that takes a string argument and calls the base class constructor with it.
                CodeConstructor baseStringConstructor = new CodeConstructor();
                baseStringConstructor.Attributes = MemberAttributes.Public;
                baseStringConstructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(String), "serviceName"));
                baseStringConstructor.BaseConstructorArgs.Add(new CodeVariableReferenceExpression("serviceName"));
                NewClass.Members.Add(baseStringConstructor);

            }
            ns.Types.Add(NewClass);
            return ns;
        }

        private static CodeMemberMethod CreateServiceClientMethods(CodeMemberMethod IMethod, CodeTypeReference IType)
        {
            CodeMemberMethod ReturnValue = new CodeMemberMethod();
            ReturnValue = (CodeMemberMethod)ObjectDeepCopy((Object)IMethod);

            // Ersetzt den Typ TItem in den Method Parameters 
            foreach (CodeParameterDeclarationExpression MethodParam in ReturnValue.Parameters)
                if (MethodParam.Type.BaseType.ToUpperInvariant() == "TItem".ToUpperInvariant())
                    MethodParam.Type.BaseType = IType.TypeArguments[0].BaseType;

            // Ersetzt den Typ TItem in den Method ReturnTypes
            if (ReturnValue.ReturnType.BaseType.ToUpperInvariant().Contains("TItem".ToUpperInvariant()))
            {
                ReturnValue.ReturnType.BaseType = ReturnValue.ReturnType.BaseType.Replace("TItem", IType.TypeArguments[0].BaseType);
            }
            
            String httpMethodUsed = "";
            foreach (CodeAttributeDeclaration customAttributes in ReturnValue.CustomAttributes)
            {
                if (customAttributes.Name.ToUpperInvariant() == "WebGet".ToUpperInvariant())
                {
                    httpMethodUsed = "GET";
                }
                else if (customAttributes.Name.ToUpperInvariant() == "WebInvoke".ToUpperInvariant())
                {
                    foreach (CodeAttributeArgument attribArguements in customAttributes.Arguments)
                    {
                        if (attribArguements.Name.ToUpperInvariant() == "Method".ToUpperInvariant())
                        {
                            CodePrimitiveExpression WebInvokeDeclaration = (CodePrimitiveExpression) attribArguements.Value;
                            httpMethodUsed = (String) WebInvokeDeclaration.Value;
                        }
                    }
                }
            }

            String methodName = "";
            switch (httpMethodUsed)
            {
                case "GET":
                    if (ReturnValue.ReturnType.BaseType == String.Format("IDictionary<string,{0}>", IType.TypeArguments[0].BaseType))
                    {
                        methodName = "OnGetItems";
                    }
                    else
                    {
                        methodName = "OnGetItem";
                    }
                    break;
                case "POST":
                    methodName = "OnAddItem";
                    break;
                case "PUT":
                    methodName = "OnUpdateItem";
                    break;
                case "DELETE":
                    methodName = "OnDeleteItem";
                    break;
            }

            System.Collections.Generic.List<CodeExpression> methodParameters = new System.Collections.Generic.List<CodeExpression>();
            foreach (CodeParameterDeclarationExpression mparam in ReturnValue.Parameters)
            {
                methodParameters.Add(new CodeVariableReferenceExpression(mparam.Name));    
            }
            
            CodeMethodInvokeExpression methodInvokeExpression = new CodeMethodInvokeExpression(
                new CodeBaseReferenceExpression(),
                methodName, methodParameters.ToArray());

            if (ReturnValue.ReturnType.BaseType == typeof(void).FullName)
            {
                ReturnValue.Statements.Add(methodInvokeExpression);
            }
            else
            {
                ReturnValue.Statements.Add(new CodeMethodReturnStatement(methodInvokeExpression));
            }

            ReturnValue.CustomAttributes.Clear();
            return ReturnValue;
            }

        private static Object ObjectDeepCopy(Object Value) 
        {
            Object ReturnValue = null;
            // Deep copy des Objekts. So wird kein Zeiger auf das
            // alte Objekt gespeichert -> sondern eine neue
            // unabhängige Instanz erzeugt:
            using (MemoryStream ms = new MemoryStream())
            {
                System.Runtime.Serialization.Formatters.Binary.BinaryFormatter fmt = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

                // Original serialisieren:
                fmt.Serialize(ms, Value);

                // Position des Streams auf den Anfang zurücksetzen:
                ms.Position = 0;

                // Kopie erstellen:
                ReturnValue = fmt.Deserialize(ms) as Object; 
            }
            return ReturnValue;        
        }
       
        private static CodeMemberMethod GetMethodDefinitionFromWadl(method WadlMethod, application WadlRoot)
        {
            if (WadlMethod.href == null)
            {   // is a Method Definition
                if (WadlMethod.response != null)
                {
                    foreach (Sun.Wadl.response elem_response in WadlMethod.response)
                    {
                        if (elem_response.representation != null)
                        {
                            foreach (Sun.Wadl.representation elem_representation in elem_response.representation)
                            {
                                if (elem_representation.mediaType.ToUpperInvariant() == "application/xml".ToUpperInvariant())
                                {
                                    return GlobalMethodDefinitionFromWadl(WadlMethod);
                                }
                            }
                        }
                    }
                    return null;
                }
                else
                {
                    return GlobalMethodDefinitionFromWadl(WadlMethod);
                }
                
            }
            else
            {   // is a Method Reference ==> Search for GlobalDefinition...
                if (WadlMethod.Any != null || WadlMethod.AnyAttr != null || WadlMethod.doc != null || WadlMethod.id != null || WadlMethod.name != null || WadlMethod.request != null || WadlMethod.response != null)
                {
                    throw new ArgumentException(
                        String.Format(
                        System.Globalization.CultureInfo.InvariantCulture,
                        "A method reference element MUST NOT have any otherWADL-defined attributes or contain anyWADL-defined child elements."
                        ));
                }

                String[] hrefUri = WadlMethod.href.Split('#');
                String MethodURI = hrefUri[0];
                String MethodAnchor = hrefUri[hrefUri.Length-1];

                if (String.IsNullOrEmpty(MethodURI))
                {   // locally defined - search in WadlRoot for definition
                    if (WadlRoot.Items != null)
                    {
                        foreach (var unknownElement in WadlRoot.Items)
                        {
                            Sun.Wadl.method WadlGlobalMethodDefinition = unknownElement as method;
                            if (WadlGlobalMethodDefinition != null)
                            {
                                if (WadlGlobalMethodDefinition.id.ToUpperInvariant() == MethodAnchor.ToUpperInvariant())
                                {
                                    return GetMethodDefinitionFromWadl(WadlGlobalMethodDefinition, WadlRoot);
                                }
                            }
                        }
                    }
                }
                else
                {   // must be defined elsewhere?!?
                }
                return null;
            }
        }

        private static CodeMemberMethod GlobalMethodDefinitionFromWadl(method WadlMethod)
        {
            CodeAttributeDeclaration AttribOpCon = new CodeAttributeDeclaration("OperationContract");
            CodeAttributeDeclaration AttribWebInv = new CodeAttributeDeclaration("WebGet");

            CodeMemberMethod ReturnValue = new CodeMemberMethod();
            ReturnValue.Name = WadlMethod.id;
            ReturnValue.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            ReturnValue.CustomAttributes.Add(AttribOpCon);
            ReturnValue.CustomAttributes.Add(AttribWebInv);

            if (WadlMethod.name.ToUpperInvariant() != "GET".ToUpperInvariant())
            {
                AttribWebInv.Name = "WebInvoke";
                AttribWebInv.Arguments.Add(new CodeAttributeArgument("Method", new CodePrimitiveExpression(WadlMethod.name.ToUpperInvariant())));
            }

            bool isUriTemplateSet = false;
            if (WadlMethod.request != null)
            {
                if (WadlMethod.request.param != null)
                {
                    foreach (var param in WadlMethod.request.param)
                    {
                        CodeParameterDeclarationExpression newParam = new CodeParameterDeclarationExpression();
                        newParam.Type = GetTypeFromWadlType(param);
                        if (newParam.Type.BaseType != "TItem")
                        {
                            AttribWebInv.Arguments.Add(new CodeAttributeArgument("UriTemplate", new CodePrimitiveExpression("{" + param.name + "}")));
                        }
                        newParam.Name = param.name;
                        ReturnValue.Parameters.Add(newParam);
                    }
                    
                    foreach (CodeAttributeArgument Attrib in AttribWebInv.Arguments)
                    {
                        if (Attrib.Name == "UriTemplate")
                        {
                            isUriTemplateSet = true;
                        }
                    }
                }
            }
            if (!isUriTemplateSet)
            {
                AttribWebInv.Arguments.Add(new CodeAttributeArgument("UriTemplate", new CodePrimitiveExpression("")));
            }

            if (WadlMethod.response != null)
            {
                if (WadlMethod.response.Length > 1)
	            {
                    throw new ArgumentException(
                        String.Format(
                            System.Globalization.CultureInfo.InvariantCulture,
                            "Wadl File contains a Method "+WadlMethod.id+" with more than one response parameters."
                        ));
	            }  
                
                foreach (Sun.Wadl.response ReturnType in WadlMethod.response)
                {
                    
                    if (ReturnType.param != null)
                    {
                        foreach (Sun.Wadl.param param in ReturnType.param)
                        {
                            if (param.repeating)
                            {
                                ReturnValue.ReturnType.BaseType = "IDictionary<string,TItem>";
                            }
                            else 
                            {
                                ReturnValue.ReturnType.BaseType = "TItem";
                            }
                        }
                    }
                }
            }
            return ReturnValue;
        }

        private static CodeTypeReference GetTypeFromWadlType(param Value)
        {
            CodeTypeReference ReturnValue = null;
            if (Value.type != null)
            {
                switch (Value.type.Name.Trim().ToUpperInvariant())
                {
                    case "STRING":
                        ReturnValue = new CodeTypeReference(typeof(String));
                        break;
                    case "INT":
                    case "INTEGER":
                        ReturnValue = new CodeTypeReference(typeof(Int32));
                        break;
                    default:
                        ReturnValue = new CodeTypeReference("TItem");
                        break;
                }
            }
            return ReturnValue;
        }
        
        private void xmlser_UnknownNode(object sender, XmlNodeEventArgs e)
        {
            Console.WriteLine("Fehler in XML-Dokument." + e.Name);
        }

        private String GetInputFileName()
        {
            String outputFileName = "";
            String[] tmpWadlFileName = m_WadlFileName.Split('\\');
            tmpWadlFileName = tmpWadlFileName[tmpWadlFileName.Length - 1].Split('.');
            for (int i = 0; i < tmpWadlFileName.Length - 1; i++)
            {
                outputFileName += tmpWadlFileName[i];
            }
            return outputFileName;
        }
        
        #endregion

        #region public methods

        /// <summary>
        /// Class Constructor
        /// </summary>
        /// <param name="fileName">Path to wadl file</param>
        /// <param name="codeNamespace">Code namespace for code generated</param>
        public WadlToCodeGenerator(String fileName, String codeNamespace)
        {   // Constructor
            m_CodeNamespaceName = (codeNamespace == null) ? "CodeGenereatedFromWADL" : codeNamespace;
            m_WadlFileName = fileName;

            application WadlRootElement;
            using (FileStream fs = File.Open(fileName, FileMode.Open))
            {
                XmlSerializer xmlser = new XmlSerializer(typeof(application));
                xmlser.UnknownNode += new XmlNodeEventHandler(xmlser_UnknownNode);
                WadlRootElement = (application)xmlser.Deserialize(fs);
            }
            m_RescourceClass = CreateResourceClassFromWadl(WadlRootElement, m_CodeNamespaceName);
            m_ServiceContract = CreateServiceContractInterfaceFromWadl(WadlRootElement, m_CodeNamespaceName);
            m_ServiceClient = CreateServiceClientImplementation(m_CodeNamespaceName);
        }
       
        /// <summary>
        /// Stores the generated code to the filesystem at the specified location
        /// </summary>
        /// <param name="fileType"></param>
        /// <param name="outputDirectory">Local path of the file to be generated</param>
        /// <param name="outputFileName">The filename without fileextension</param>
        //[CustomPermissionAttribute(SecurityAction.Demand, Unrestricted = true)]
        [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, Unrestricted=true)]
        public void GetGeneratedCodeAsFile(GeneratedFileType fileType, String outputDirectory, String language)
        {
            String outputFileName = GetClassNameFor(fileType);
            CodeNamespace GeneratedCodeNamespace = GetNamespaceFor(fileType);

            CodeCompileUnit unit = new CodeCompileUnit();
            unit.Namespaces.Add(GeneratedCodeNamespace);
            CodeDomProvider provider = CodeDomProvider.CreateProvider(language);
            using (TextWriter tw = File.CreateText(Path.Combine(outputDirectory, outputFileName + '.' + provider.FileExtension)))
            {
                provider.GenerateCodeFromCompileUnit(unit, tw, null);
            }
        }

        /// <summary>Create a default config file for the created service client</summary>
        /// <param name="outputdir">The output dir for the config file</param>
        public void CreateDefaultConfigFile(string outputdir)
        {
            string contract = GetClassNameFor(GeneratedFileType.ServiceContract);
            string serviceClient = GetClassNameFor(GeneratedFileType.ServiceClient);
            XmlWriterSettings settings = new XmlWriterSettings() { Indent = true, CloseOutput = true, NewLineOnAttributes = false };
            using (XmlWriter xw = XmlWriter.Create(Path.Combine(outputdir, "App.config"), settings))
            {
                xw.WriteStartDocument();
                xw.WriteStartElement("configuration");
                xw.WriteStartElement("system.serviceModel");
                xw.WriteStartElement("bindings");
                xw.WriteStartElement("basicHttpBinding");
                xw.WriteEndElement();
                xw.WriteEndElement();
                xw.WriteStartElement("client");
                xw.WriteStartElement("endpoint");
                xw.WriteAttributeString("address", "http://InsertRuntimeAdressHere:port/Service.svc");
                xw.WriteAttributeString("binding", "basicHttpBinding");
                xw.WriteAttributeString("contract", contract);
                xw.WriteAttributeString("name", serviceClient);
                xw.WriteEndElement();
                xw.WriteEndElement();
                xw.WriteEndElement();
                xw.WriteEndElement();
                xw.WriteEndDocument();
            }
        }

        #endregion

    }
}
